Template-in-HTML vs Markup-as-Code

  • I want static.

  • Maybe HTMX is overkill for that and requires me to think if I use templates or markup-as-code.

  • I can maybe run away from JS, but there's no running from HTML and CSS.

  • I need to learn HTML, regardless if I use Hugo, or Odin + HTMX or Odin + Templating, or something hybrid.

  • I imagine that if I want an easy front-end, I would go for templating, and therefore, Hugo.

  • If I want templating, why would I consider doing something myself, in Odin?

    • Hugo does things for me, but I don't like how opaque things can be, or how I have to follow the conventions defined by Hugo, without really knowing what happens.

    • So, Hugo hides the backend, and hides how the front-end is built, inside layers of documentation.

    • If I expose the backend with Odin, I could understand everything.

  • What would be needed, if I choose to make a backend in Odin:

    • HTTP routing.

    • Support for the templating used by Go.

    • Parsing of the Markdown file, so its used correctly by the templating service.

    • A way that I can open a server.

      • Could my game_net  library do that? I think so.

  • I don't want to use WASM. I lose themes and convenience. Performance may not be optimal. Over complicates with Odin, while being opaque and with no front-end.

Template-in-HTML Engines

  • Uses declarative HTML files (or partials) with embedded logic (templating syntax).

Etc

HTMX
Alpine Ajax
Alpine.js

Jinja2 Family

{% for u in users %}
  <li>{{ u }}</li>
{% endfor %}
Temple (Odin)
Go Templates
  • Server-side only, but outputs static HTML usable client-side.

  • Hugo uses it.

<ul>
  {{ range .Pages }}
    <li><a HREF="{{ .RelPermalink }}">{{ .Title }}</a></li>
  {{ else }}
    <li>No pages</li>
  {{ end }}
</ul>
  • "Functionally similar to Jinja2 but in Go."

Plush (Go)
  • Mimics Jinja2.

Pongo2 (Go)
  • Server-side.

  • Mimics Jinja2.

Nunjucks (Node.js)
  • Client-side.

  • Mozilla’s Jinja2 port for Node.js.

const nunjucks = require("nunjucks");

const env = new nunjucks.Environment();
const template = `
<ul>
  {% for u in users %}
    <li>{{ u }}</li>
  {% endfor %}
</ul>`;

console.log(env.renderString(template, { users: ["Alice", "Bob"] }));
Jinja2 (Flask/Django Template)
  • Server-side.

  • Original source of the style ( {% for %} , {{ var }} ).

# app.py
from jinja2 import Template

template = Template("""
<ul>
{% for u in users %}
  <li>{{ u }}</li>
{% endfor %}
</ul>
""")

print(template.render(users=["Alice", "Bob"]))
Twig (PHP)
  • Server-side.

  • Direct port of Jinja2; nearly identical syntax.

<?php
require_once '/path/to/vendor/autoload.php';

$loader = new \Twig\Loader\ArrayLoader([
    'list' => '<ul>{% for u in users %}<li>{{ u }}</li>{% endfor %}</ul>',
]);
$twig = new \Twig\Environment($loader);

echo $twig->render('list', ['users' => ['Alice', 'Bob']]);
Pebble (Java)
  • Server-side.

  • Jinja2-inspired syntax for Java.

Liquid Family

  • Liquid was directly inspired by Jinja2, but the design goals differ.

  • Main distinction: Liquid is safe and restricted, Jinja2 is powerful and programmable.

{% for u in users %}
  <li>{{ u.name }}</li>
{% endfor %}
Liquid (Go)
  • Server-side.

  • Port with near-identical tags.

package main

import (
    "fmt"
    "github.com/osteele/liquid"
)

func main() {
    tmpl := liquid.NewTemplate(`
<ul>
{% for u in users %}
  <li>{{ u }}</li>
{% endfor %}
</ul>`)
    out, _ := tmpl.Render(map[string][]string{"users": {"Alice", "Bob"}})
    fmt.Println(out)
}
Liquid (Ruby)
  • Server-side.

  • Canonical version.

Liquid-php (PHP)
  • Server-side.

  • Syntax and filters same as Ruby’s version.

Mustache Family

<ul>
  {{#users}}
    <li>{{name}}</li>
  {{/users}}
</ul>
odin-mustache (Odin)
Mustache (Go - hoisie/mustache)
  • Server-side.

package main

import (
    "fmt"
    "github.com/hoisie/mustache"
)

func main() {
    tpl := "<ul>{{#users}}<li>{{.}}</li>{{/users}}</ul>"
    html := mustache.Render(tpl, map[string][]string{"users": {"Alice", "Bob"}})
    fmt.Println(html)
}
Mustache (Go - broglie/mustache)
  • Server-side.

Handlebars (JavaScript Node.js)
  • Client-side.

const Handlebars = require("handlebars");

const source = `
<ul>
  {{#each users}}
    <li>{{this}}</li>
  {{/each}}
</ul>`;
const template = Handlebars.compile(source);
console.log(template({ users: ["Alice", "Bob"] }));
Mustache.js (JavaScript)
  • Client-side.

  • Logic-less templates.

pystache (Python)
  • Server-side.

  • Compatible implementation.

mustache.java (Java)
  • Server-side.

  • Official Java port.

mustache.php (PHP)
  • Server-side.

  • Official PHP port.

mustache.rb (Ruby)
  • Server-side.

  • Original reference version.

Embedded Code Families

<ul>
<% users.each do |u| %>
  <li><%= u %></li>
<% end %>
</ul>
  • Pattern <% code %>  and <%= output %> .

EJS (Node.js)
  • Client-side.

// index.js
const ejs = require("ejs");

const template = `
<ul>
  <% users.forEach(u => { %>
    <li><%= u %></li>
  <% }) %>
</ul>`;

const html = ejs.render(template, { users: ["Alice", "Bob"] });
console.log(html);
ERB (Ruby on Rails)
  • Server-side.

require "erb"

template = <<~ERB
<ul>
  <% users.each do |u| %>
    <li><%= u %></li>
  <% end %>
</ul>
ERB

puts ERB.new(template).result_with_hash(users: ["Alice", "Bob"])
PHP (native templates)
<ul>
<?php if (!empty($items)): ?>
  <?php foreach ($items as $item): ?>
    <li><?= htmlspecialchars($item['title']) ?></li>
  <?php endforeach; ?>
<?php else: ?>
  <li>No items</li>
<?php endif; ?>
</ul>
JSP / FreeMarker (Java)
  • Server-side.

<ul>
<% String[] users = {"Alice", "Bob"}; %>
<% for (String u : users) { %>
  <li><%= u %></li>
<% } %>
</ul>

Markup-as-Code (DSL for HTML)

  • All UI layout and structure are defined through code (e.g., a DSL or API calls) instead of static HTML templates.

  • "I'm a senior programmer in C and Odin Lang. Which strategy would suit me best as a web developer?"

    • Pick Markup-as-Code  + strong typing.

    • Reason: you value explicit control, deterministic behavior, and compile-time checks. Markup-as-Code maps to those priorities better than template HTML.

    • Types and compiler safety mirror C/Odin workflows.

    • Programmatic UI lets you express invariants, abstractions, and reusable low-level primitives.

    • Easier to test, refactor, and profile.

    • Better fit for complex state and performance tuning.

  • "Now, also consider that I hate JavaScript. Would your suggestion change?"

    • Yes. Avoiding JavaScript shifts the balance. You’ll favor Markup-as-Code  in a non-JS ecosystem, ideally with a compiled language that generates web UI.

Templ (Go)
  • Templ - Go .

  • Type-safe HTML template for Go.

    • Compile-time type-safe.

  • Component Model.

    • Just like React, Vue.

  • JSX-like, but pure Go. Generates type-safe HTML components compiled to Go code.

  • Output: plain HTML at compile time. No JavaScript.

package main
import "github.com/a-h/templ"

var userList = templ.Component(func(users []User) templ.Component {
    return templ.Ul(
        templ.Range(users, func(u User) templ.Component {
            return templ.Li(templ.Text(u.Name))
        }),
    )
})
  • To use, you create a file with .templ , which is written just like any other Go file.

html/template (Go)
  • Runtime Type-safe.

    • Seems like I should simply go for Templ instead, just because of this.

  • Interpreted at runtime (slower).

  • Less component structured.

  • Demo .

JS/TS-based

JSX
  • JSX does not  strictly require React, but it originated there.

  • It is a syntax , not a framework.

  • You can use JSX with other libraries if they define a createElement  function or JSX pragma, for example:

    • Preact ( h  pragma)

    • Solid.js

    • Nerv

    • Qwik

    • Custom renderers

  • JSX is a syntax extension that allows writing HTML-like elements directly in JavaScript/TypeScript code.

  • In a .tsx  file, the TypeScript compiler translates JSX into function calls (like React.createElement ) or into the JSX syntax expected by the framework in use.

  • With React-style :

    import React from 'react';
    
    const App: React.FC = () => {
      return (
        <div>
          <h1>Hello, TypeScript with JSX!</h1>
        </div>
      );
    };
    
    export default App;
    
    export default function List({ items }) {
      if (!items.length) return <div>No items</div>;
      return (
        <ul>
          {items.map(item => <li key={item.id}>{item.title}</li>)}
        </ul>
      );
    }
    
Qwik
// UserList.qwik.tsx
import { component$ } from '@builder.io/qwik';

export const UserList = component$((props: { users: { id:number, name:string }[] }) => {
  return (
    <ul>
      {props.users.map(u => <li key={u.id}>{u.name}</li>)}
    </ul>
  );
});
Angular
Lit / Web Components
import { LitElement, html } from 'lit';

class UserList extends LitElement {
  static properties = { users: { type: Array } };
  constructor() { super(); this.users = []; }

  render() {
    return html`<ul>${this.users.map(u => html`<li>${u.name}</li>`)}</ul>`;
  }
}
customElements.define('user-list', UserList);
Preact
Solid
Vue
Svelte

Based on bad languages

Yew (Rust)
  • React-like, with macro-based templating.

  • Client-side via WebAssembly (SPA-style)

  • Performance lighter, Solid.js-like

  • JSX-like macros

  • Full SPA in WASM

use leptos::*;

#[component]
pub fn UserList(cx: Scope, users: Vec<String>) -> impl IntoView {
    view! { cx,
        <ul>
            {users.into_iter().map(|u| view!{ cx, <li>{u}</li> }).collect::<Vec<_>>()}
        </ul>
    }
}
Leptos (Rust)
  • Reactive, component-based, JSX-style but all Rust.

  • Compiles to WebAssembly.

  • Hybrid: server-side rendering + hydration (like Next.js)

  • Performance heavier, React-like.

  • Reactive SSR or hybrid web app

html! {
    <ul>
        { for users.iter().map(|u| html!{ <li>{ &u.name }</li> }) }
    </ul>
}
Pynecone / Reflex (Python)
import reflex as rx

def users_view(users):
    return rx.ul(rx.foreach(users, lambda u: rx.li(u.name)))
NiceGUI (Python)
  • Python API generating web UIs with no HTML coding.

Pug (Node.js)
  • Formerly Jade.

  • Write HTML with indentation-based syntax instead of angle brackets.

const pug = require("pug");

const template = `
ul
  each u in users
    li= u
`;

const html = pug.render(template, { users: ["Alice", "Bob"] });
console.log(html);
Flutter (Dart)
  • Widget tree is code.

ListView(
  children: users.map((u) => Text(u.name)).toList(),
);
SwiftUI (Swift)
List(users) { user in
  Text(user.name)
}
Jetpack Compose (Kotlin)
LazyColumn {
  items(users) { user ->
    Text(user.name)
  }
}
Blazor (C#)
  • With Razor components.

<ul>
  @foreach (var user in Users)
  {
    <li>@user.Name</li>
  }
</ul>